Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / androidApp / src / main / kotlin / org / meshtastic / app / MeshUtilApplication.kt

Displaying Raw • Download

androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt renovate/spotless (366f71f4) Text, 11.62 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.app

Tff7b72import T7ee787android.app.ActivityManager
Tff7b72import T7ee787android.app.Application
Tff7b72import T7ee787android.appwidget.AppWidgetProviderInfo
Tff7b72import T7ee787android.content.Context
Tff7b72import T7ee787android.os.Build
Tff7b72import T7ee787androidx.annotation.RequiresApi
Tff7b72import T7ee787androidx.annotation.VisibleForTesting
Tff7b72import T7ee787androidx.collection.intSetOf
Tff7b72import T7ee787androidx.glance.appwidget.GlanceAppWidgetManager
Tff7b72import T7ee787androidx.work.Configuration
Tff7b72import T7ee787androidx.work.ExistingPeriodicWorkPolicy
Tff7b72import T7ee787androidx.work.PeriodicWorkRequestBuilder
Tff7b72import T7ee787androidx.work.WorkManager
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787co.touchlab.kermit.Severity
Tff7b72import T7ee787coil3.ImageLoader
Tff7b72import T7ee787coil3.SingletonImageLoader
Tff7b72import T7ee787kotlinx.coroutines.CoroutineExceptionHandler
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.TimeoutCancellationException
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.runBlocking
Tff7b72import T7ee787kotlinx.coroutines.withTimeout
Tff7b72import T7ee787org.koin.android.ext.android.get
Tff7b72import T7ee787org.koin.android.ext.koin.androidContext
Tff7b72import T7ee787org.koin.androidx.workmanager.koin.workManagerFactory
Tff7b72import T7ee787org.koin.plugin.module.dsl.startKoin
Tff7b72import T7ee787org.meshtastic.app.di.AndroidKoinApp
Tff7b72import T7ee787org.meshtastic.core.common.ContextServices
Tff7b72import T7ee787org.meshtastic.core.database.DatabaseManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshPrefs
Tff7b72import T7ee787org.meshtastic.core.repository.PlatformAnalytics
Tff7b72import T7ee787org.meshtastic.core.repository.ServiceRepository
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.discovery_interrupted_scan_restored
Tff7b72import T7ee787org.meshtastic.core.resources.getStringSuspend
Tff7b72import T7ee787org.meshtastic.core.service.worker.MeshLogCleanupWorker
Tff7b72import T7ee787org.meshtastic.feature.discovery.DiscoveryScanEngine
Tff7b72import T7ee787org.meshtastic.feature.widget.LocalStatsWidgetReceiver
Tff7b72import T7ee787kotlin.time.Duration.Companion.hours
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds
Tff7b72import T7ee787kotlin.time.toJavaDuration

T8b949e/**
* [android.app.ApplicationExitInfo.getDescription] reported by Android 17's per-app memory ceiling (zram-swap kill).
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3MEMORY_LIMITER_ANON_SWAP_REASON Tff7b72= Ta5d6ff"Ta5d6ffMemoryLimiter:AnonSwapTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3MEMORY_LIMITER_PREFS_NAME Tff7b72= Ta5d6ff"Ta5d6ffmemory_limiter_exit_checkTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3KEY_LAST_REPORTED_EXIT_TIMESTAMP Tff7b72= Ta5d6ff"Ta5d6fflast_reported_exit_timestampTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3MEMORY_LIMITER_SCAN_DEPTH Tff7b72= T79c0ff5

T8b949e/**
* The main application class for Meshtastic.
*
* This class initializes core application components using Koin for dependency injection.
*/
Tff7b72open Tff7b72class T56d364MeshUtilApplication Tb4b4b4:
Te6edf3ApplicationTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3ConfigurationTb4b4b4.Te6edf3ProviderTb4b4b4,
Te6edf3SingletonImageLoaderTb4b4b4.Te6edf3Factory Tb4b4b4{

T8b949e// SupervisorJob alone only isolates siblings: a failed child still reaches the global uncaught
T8b949e// handler. These launches are best-effort background init, so report rather than escalate.
Tff7b72private Tff7b72val Te6edf3applicationScopeExceptionHandler Tff7b72= Te6edf3CoroutineExceptionHandler Tb4b4b4{ Te6edf3contextTb4b4b4, Te6edf3throwable Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3throwableTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffBackground application init failed in Tffd700$Te6edf3contextTa5d6ff" Tb4b4b4}
Tb4b4b4}

Tff7b72protected Tff7b72val Te6edf3applicationScope Tff7b72=
Te6edf3CoroutineScopeTb4b4b4(Te6edf3SupervisorJobTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3DispatchersTb4b4b4.Te6edf3Default Tff7b72+ Te6edf3applicationScopeExceptionHandlerTb4b4b4)

T8b949e/** Supplies Coil's process-wide loader without retaining an Activity in its singleton factory. */
Tff7b72override Tff7b72fun Td2a8ffnewImageLoaderTb4b4b4(Te6edf3contextTb4b4b4: Te6edf3ContextTb4b4b4)Tb4b4b4: Te6edf3ImageLoader Tff7b72= Tff7b72getTff7b72<Te6edf3ImageLoaderTff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffonCreateTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72superTb4b4b4.Te6edf3onCreateTb4b4b4(Tb4b4b4)
Te6edf3ContextServicesTb4b4b4.Te6edf3app Tff7b72= Tff7b72this
Te6edf3configureFlavorApplicationTb4b4b4(Te6edf3BuildConfigTb4b4b4.Te6edf3APPLICATION_IDTb4b4b4)

Te6edf3startKoinTff7b72<Te6edf3AndroidKoinAppTff7b72> Tb4b4b4{
Te6edf3androidContextTb4b4b4(Tff7b72thisTf0883e@MeshUtilApplicationTb4b4b4)
Te6edf3workManagerFactoryTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e// Schedule periodic MeshLog cleanup. Off-main: WorkManager uses on-demand init here
T8b949e// (the startup provider is removed), so getInstance() opens WorkManager's Room DB.
Te6edf3applicationScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3scheduleMeshLogCleanupTb4b4b4(Tb4b4b4) Tb4b4b4}

T8b949e// ApplicationExitInfo requires API 30+. A "MemoryLimiter:AnonSwap" reason means Android 17's per-app
T8b949e// memory ceiling zram-swapped then killed the previous process — see reportMemoryLimiterExitIfPresent.
Tff7b72if Tb4b4b4(Te6edf3BuildTb4b4b4.Te6edf3VERSIONTb4b4b4.Te6edf3SDK_INT Tff7b72>Tff7b72= Te6edf3BuildTb4b4b4.Te6edf3VERSION_CODESTb4b4b4.Te6edf3RTb4b4b4) Tb4b4b4{
Te6edf3applicationScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3reportMemoryLimiterExitIfPresentTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

T8b949e// Generate and publish widget preview for Android 15+ widget picker
Tff7b72if Tb4b4b4(Te6edf3BuildTb4b4b4.Te6edf3VERSIONTb4b4b4.Te6edf3SDK_INT Tff7b72>Tff7b72= Te6edf3BuildTb4b4b4.Te6edf3VERSION_CODESTb4b4b4.Te6edf3VANILLA_ICE_CREAMTb4b4b4) Tb4b4b4{
Te6edf3applicationScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72suspend Tff7b72fun Td2a8ffpushPreviewTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffPushing generated widget preview...Ta5d6ff" Tb4b4b4}
Tff7b72val Te6edf3result Tff7b72=
Te6edf3GlanceAppWidgetManagerTb4b4b4(Tff7b72thisTf0883e@MeshUtilApplicationTb4b4b4)
Tb4b4b4.Te6edf3setWidgetPreviewsTb4b4b4(
Te6edf3LocalStatsWidgetReceiverTff7b72::Te6edf3classTb4b4b4,
Te6edf3intSetOfTb4b4b4(Te6edf3AppWidgetProviderInfoTb4b4b4.Te6edf3WIDGET_CATEGORY_HOME_SCREENTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffsetWidgetPreviews result: Tffd700$Te6edf3resultTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to set widget previewTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Te6edf3pushPreviewTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3widgetStateProviderTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3featureTb4b4b4.Te6edf3widgetTb4b4b4.Te6edf3LocalStatsWidgetStateProvider Tff7b72= Tff7b72getTb4b4b4(Tb4b4b4)
Tff7b72try Tb4b4b4{
T8b949e// Wait for real data for up to 30 seconds before pushing an updated preview
Te6edf3withTimeoutTb4b4b4(T79c0ff3T79c0ff0.Te6edf3secondsTb4b4b4) Tb4b4b4{
Te6edf3widgetStateProviderTb4b4b4.Te6edf3stateTb4b4b4.Te6edf3first Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3showContent Tff7b72&Tff7b72& Tffa657itTb4b4b4.Te6edf3nodeShortName Tff7b72!Tff7b72= Tff7b72null Tb4b4b4}
Tb4b4b4}

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffReal node data acquired. Pushing updated widget preview.Ta5d6ff" Tb4b4b4}
Te6edf3pushPreviewTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3TimeoutCancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3iTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffTimed out waiting for real node data for widget preview.Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e// Initialize DatabaseManager asynchronously with current device address so DAO consumers have an active DB
Te6edf3applicationScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3dbManagerTb4b4b4: Te6edf3DatabaseManager Tff7b72= Tff7b72getTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3meshPrefsTb4b4b4: Te6edf3MeshPrefs Tff7b72= Tff7b72getTb4b4b4(Tb4b4b4)
Te6edf3dbManagerTb4b4b4.Te6edf3initTb4b4b4(Te6edf3meshPrefsTb4b4b4.Te6edf3deviceAddressTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e// Restore a radio left detuned by a discovery scan that was interrupted (crash, BLE loss, process death)
T8b949e// before it could restore the home LoRa config itself. Never returns; watches reconnects for the app's life.
T8b949e// The engine stays UI-free — we localize the "restored" notice here and push it through the app-wide alert.
Te6edf3applicationScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3scanEngineTb4b4b4: Te6edf3DiscoveryScanEngine Tff7b72= Tff7b72getTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3serviceRepositoryTb4b4b4: Te6edf3ServiceRepository Tff7b72= Tff7b72getTb4b4b4(Tb4b4b4)
Te6edf3scanEngineTb4b4b4.Te6edf3restoreInterruptedSessionsOnReconnect Tb4b4b4{ Te6edf3homePreset Tff7b72-Tff7b72>
Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(
Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3discovery_interrupted_scan_restoredTb4b4b4, Te6edf3homePresetTb4b4b4)Tb4b4b4,
Te6edf3SeverityTb4b4b4.Te6edf3WarnTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Stops the background init launched by [onCreate]. Robolectric never calls [onTerminate], so a unit test that
* boots this Application must call this itself — otherwise those jobs outlive the test on real
* [Dispatchers.Default] threads and their failures surface against whichever test is running next.
*/
Tf0883e@VisibleForTesting
Tff7b72fun Td2a8ffcancelBackgroundInitTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3applicationScopeTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffonTerminateTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// cancel() not cancelAndJoin(): joining under runBlocking on the main thread can deadlock.
Te6edf3cancelBackgroundInitTb4b4b4(Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3runBlocking Tb4b4b4{ Tff7b72getTff7b72<Te6edf3DatabaseManagerTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3closeTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4} Tff7b72finally Tb4b4b4{
Tff7b72superTb4b4b4.Te6edf3onTerminateTb4b4b4(Tb4b4b4)
Te6edf3orgTb4b4b4.Te6edf3koinTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3contextTb4b4b4.Te6edf3stopKoinTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@RequiresApiTb4b4b4(Te6edf3BuildTb4b4b4.Te6edf3VERSION_CODESTb4b4b4.Te6edf3RTb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffreportMemoryLimiterExitIfPresentTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// getHistoricalProcessExitReasons() returns a persistent record, not a queue, so the timestamp
T8b949e// watermark keeps a kill from being re-reported on every cold start. Scanning a few records
T8b949e// (newest first) instead of just index 0 matters: a crash during the restart that follows a
T8b949e// limiter kill slides the AnonSwap record down the list, and the newest entry alone would miss it.
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3getSharedPreferencesTb4b4b4(Te6edf3MEMORY_LIMITER_PREFS_NAMETb4b4b4, Te6edf3ContextTb4b4b4.Te6edf3MODE_PRIVATETb4b4b4)
Tff7b72val Te6edf3watermark Tff7b72= Te6edf3prefsTb4b4b4.Te6edf3getLongTb4b4b4(Te6edf3KEY_LAST_REPORTED_EXIT_TIMESTAMPTb4b4b4, Tff7b72-T79c0ff1LTb4b4b4)
Tff7b72val Te6edf3kill Tff7b72=
Te6edf3getSystemServiceTb4b4b4(Te6edf3ActivityManagerTff7b72::Te6edf3classTb4b4b4.Te6edf3javaTb4b4b4)
Tff7b72?.Te6edf3getHistoricalProcessExitReasonsTb4b4b4(Tff7b72nullTb4b4b4, T79c0ff0Tb4b4b4, Te6edf3MEMORY_LIMITER_SCAN_DEPTHTb4b4b4)
Tff7b72?.Te6edf3firstOrNull Tb4b4b4{
T8b949e// contains(), not equality: OEMs may decorate the description as the rollout expands.
Tffa657itTb4b4b4.Te6edf3descriptionTff7b72?.Te6edf3containsTb4b4b4(Te6edf3MEMORY_LIMITER_ANON_SWAP_REASONTb4b4b4) Tff7b72=Tff7b72= Tff7b72true Tff7b72&Tff7b72& Tffa657itTb4b4b4.Te6edf3timestamp Tff7b72> Te6edf3watermark
Tb4b4b4} Tff7b72?: Tff7b72return

T8b949e// Synchronous commit() before telemetry: this path runs precisely when the OS has been killing this
T8b949e// process, and an apply() lost to another death would re-report the same kill. Background thread.
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3prefsTb4b4b4.Te6edf3editTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3putLongTb4b4b4(Te6edf3KEY_LAST_REPORTED_EXIT_TIMESTAMPTb4b4b4, Te6edf3killTb4b4b4.Te6edf3timestampTb4b4b4)Tb4b4b4.Te6edf3commitTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72return

T8b949e// Force PlatformAnalytics construction so its Kermit log writers (Crashlytics/Datadog) are
T8b949e// registered before we log, even though nothing else has injected it yet this early in startup.
Tff7b72getTff7b72<Te6edf3PlatformAnalyticsTff7b72>Tb4b4b4(Tb4b4b4)

T8b949e// Unlike the recoverable, environment-driven states ExpectedCondition.kt guards against (BLE off,
T8b949e// a denied permission), a memory-ceiling kill means this process's own footprint was too large —
T8b949e// an actionable engineering signal, so it is deliberately reported as a non-fatal (Error), not a
T8b949e// rate-only breadcrumb.
T8b949e// getPss()/getRss() return 0 when the system didn't capture them; don't report that as "0KB".
Tff7b72fun Td2a8ffformatKbTb4b4b4(Te6edf3kiloBytesTb4b4b4: Tffa657LongTb4b4b4) Tff7b72= Tff7b72if Tb4b4b4(Te6edf3kiloBytes Tff7b72> T79c0ff0Tb4b4b4) Ta5d6ff"Tffd700${Te6edf3kiloBytesTffd700}Ta5d6ffKBTa5d6ff" Tff7b72else Ta5d6ff"Ta5d6ffunreportedTa5d6ff"
Te6edf3LoggerTb4b4b4.Te6edf3e Tb4b4b4{
Ta5d6ff"Ta5d6ffPrevious process exit: Tffd700$Te6edf3MEMORY_LIMITER_ANON_SWAP_REASONTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffpss=Tffd700${Te6edf3formatKbTb4b4b4(Te6edf3killTb4b4b4.Te6edf3pssTb4b4b4)Tffd700}Ta5d6ff rss=Tffd700${Te6edf3formatKbTb4b4b4(Te6edf3killTb4b4b4.Te6edf3rssTb4b4b4)Tffd700}Ta5d6ff"
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffscheduleMeshLogCleanupTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3cleanupRequest Tff7b72=
Te6edf3PeriodicWorkRequestBuilderTff7b72<Te6edf3MeshLogCleanupWorkerTff7b72>Tb4b4b4(Te6edf3repeatInterval Tff7b72= T79c0ff1.Te6edf3hoursTb4b4b4.Te6edf3toJavaDurationTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)

Te6edf3WorkManagerTb4b4b4.Te6edf3getInstanceTb4b4b4(Tff7b72thisTb4b4b4)
Tb4b4b4.Te6edf3enqueueUniquePeriodicWorkTb4b4b4(
Te6edf3MeshLogCleanupWorkerTb4b4b4.Te6edf3WORK_NAMETb4b4b4,
Te6edf3ExistingPeriodicWorkPolicyTb4b4b4.Te6edf3UPDATETb4b4b4,
Te6edf3cleanupRequestTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72val Te6edf3workManagerConfigurationTb4b4b4: Te6edf3Configuration
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3ConfigurationTb4b4b4.Te6edf3BuilderTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3setWorkerFactoryTb4b4b4(Tff7b72getTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.07s